/*************************************************************************
 * Copyright 2008 David Almilli
 *  
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 *  
 *     http://www.apache.org/licenses/LICENSE-2.0 
 *      
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 **************************************************************************/
package com.tivo.hme.bananas;

import static com.tivo.hme.bananas.IBananasPlus.*;

import com.tivo.hme.bananas.BSkinPlus.Element;
import com.tivo.hme.bananas.layout.Layout;
import com.tivo.hme.sdk.HmeEvent;
import com.tivo.hme.sdk.Resource;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.HashMap;
import java.util.Map;

public class BKeyboardPlus extends BViewPlus implements PlusView {
    private static final int EVT_KBD_VALUE = HmeEvent.EVT_RESERVED + 20;

    public static final String LOWERCASE = "lowercase";
    public static final String UPPERCASE = "uppercase";
    public static final String SYMBOL = "symbol";
    public static final String EMAIL_LOWERCASE = "email-lowercase";
    public static final String EMAIL_UPPERCASE = "email-uppercase";
    public static final String EMAIL_SYMBOL = "email-symbol";
    public static final String URL_LOWERCASE = "url-lowercase";
    public static final String URL_UPPERCASE = "url-uppercase";
    public static final String URL_SYMBOL = "url-symbol";

    public static final String PLAIN_KEYBOARD = LOWERCASE;
    public static final String EMAIL_KEYBOARD = EMAIL_LOWERCASE;
    public static final String URL_KEYBOARD = URL_LOWERCASE;
    public static final String TALL_KEYBOARD = "tall";

    public static final int INPUT_WIDTH_SAME_AS_WIDGET = -1;
    
    private boolean disposeResourceOnRemove;

    /**
     * focus manager associated with keyboard
     */
    protected BFocusMgr focusMgr = null;

    protected boolean highlightVis = false;

    /**
     * background view onto which the keys will be placed.
     */
    protected BView kbBgImage;

    protected BView kbBgImage_top;

    protected BView kbBgImage_middle;

    protected BView kbBgImage_bottom;

    protected BView dataField;

    protected BView dataFieldLeft;

    protected BView dataFieldMiddle;

    protected BView dataFieldRight;

    protected BView tipsView;

    protected BHighlightsPlus keyboardHighlights;

    protected BHighlightsPlus dataInputHighlights;

    /**
     * default selected cell
     */
    protected CellView defaultCell;

    /**
     * currently focused cell
     */
    protected CellView focused;

    /**
     * The list of all cell views
     */
    protected CellView[][] gridViews;

    /**
     * speed in which highlight moves
     */
    protected String animation;

    /**
     * The datafield in which the current value will be displayed
     */
    protected DisplayText textField;

    protected Keyboard activeKeyboard;

    protected Map<String, Keyboard> keyboardStateMap;

    /**
     * current value of the keyboard
     */
    protected String value = "";

    /**
     * current value of the keyboard
     */
    protected String undoValue = null;

    public BKeyboardPlus(BView parent, int x, int y, String keyboardName) {
        this(parent, x, y, new SkinKeyboard((BSkinPlus) parent.getBApp().getSkin(), keyboardName));
    }

    public BKeyboardPlus(BView parent, int x, int y, String keyboardName, boolean showTips) {
        this(parent, x, y, new SkinKeyboard((BSkinPlus) parent.getBApp().getSkin(), keyboardName),
                showTips);
    }

    public BKeyboardPlus(BView parent, int x, int y, Keyboard keyboard) {
        this(parent, x, y, keyboard, INPUT_WIDTH_SAME_AS_WIDGET, true);
    }

    public BKeyboardPlus(BView parent, int x, int y, Keyboard keyboard, boolean showTips) {
        this(parent, x, y, keyboard, INPUT_WIDTH_SAME_AS_WIDGET, showTips);
    }

    public BKeyboardPlus(BView parent, int x, int y, Keyboard keyboard, int textEntryWidth,
            boolean showTips) {
        this(parent, ViewUtils.getBounds(parent, x, y, getDefaultWidth(keyboard, textEntryWidth,
                showTips), keyboard.getImageHeight()), keyboard, textEntryWidth, showTips);
    }

    public BKeyboardPlus(BView parent, Layout layout, String keyboardName) {
        this(parent, layout,
                new SkinKeyboard((BSkinPlus) parent.getBApp().getSkin(), keyboardName), true);
    }

    public BKeyboardPlus(BView parent, Layout layout, String keyboardName, boolean showTips) {
        this(parent, layout,
                new SkinKeyboard((BSkinPlus) parent.getBApp().getSkin(), keyboardName), showTips);
    }

    public BKeyboardPlus(BView parent, Layout layout, Keyboard keyboard) {
        this(parent, layout, keyboard, INPUT_WIDTH_SAME_AS_WIDGET, true);
    }

    public BKeyboardPlus(BView parent, Layout layout, Keyboard keyboard, boolean showTips) {
        this(parent, layout, keyboard, INPUT_WIDTH_SAME_AS_WIDGET, showTips);
    }

    public BKeyboardPlus(BView parent, Layout layout, Keyboard keyboard, int textEntryWidth,
            boolean showTips) {
        this(parent, layout.getManager().defaultSize(layout,
                getDefaultWidth(keyboard, textEntryWidth, showTips), keyboard.getImageHeight())
                .getBounds(), keyboard, textEntryWidth, showTips);
    }

    protected BKeyboardPlus(BView parent, Rectangle bounds, Keyboard keyboard, int textEntryWidth,
            boolean showTips) {
        super(parent, bounds.x, bounds.y, bounds.width, bounds.height);

        keyboardStateMap = new HashMap<String, Keyboard>();
        keyboardStateMap.put(keyboard.getName(), keyboard);

        focusMgr = createFocusManager();
        if (keyboard == null) {
            throw new IllegalArgumentException("Keyboard can not be null");
        }

        if (textEntryWidth == INPUT_WIDTH_SAME_AS_WIDGET) {
            textEntryWidth = this.getWidth();
        }

        int dataCap = keyboard.getDataInputCap();
        int dataPadV = keyboard.getDataInputPadV();
        int dataWidth = keyboard.getDataInputImageWidth();
        int dataHeight = keyboard.getDataInputImageHeight();
        Object dataResource = keyboard.getDataInputImageResource();
        dataField = new BView(this, 0, 0, textEntryWidth, dataHeight);

        dataFieldLeft = new BView(dataField, 0, 0, dataCap, dataHeight);
        dataFieldLeft.setResource(dataResource, RSRC_HALIGN_LEFT);

        dataFieldMiddle = new BTilesPlus(dataField, dataCap, 0, textEntryWidth - 2 * dataCap,
                dataHeight, dataWidth - 2 * dataCap, dataHeight);
        dataFieldMiddle.setResource(dataResource, RSRC_HALIGN_CENTER);

        dataFieldRight = new BView(dataField, textEntryWidth - dataCap, 0, dataCap, dataHeight);
        dataFieldRight.setResource(dataResource, RSRC_HALIGN_RIGHT);

        textField = new DisplayText(dataField, dataCap, 1, dataField.getWidth() - 2 * dataCap,
                dataHeight, keyboard.getFontResource(), keyboard.getColorResource());
        textField.setFlags(RSRC_HALIGN_LEFT);

        int spacing = dataHeight + dataPadV;
        kbBgImage = new BView(this, 0, spacing, keyboard.getImageWidth(), keyboard.getImageHeight()
                - spacing);
        int t = 0;
        kbBgImage_top = new BView(kbBgImage, 0, t, keyboard.getImageWidth(), keyboard
                .getTopImageHeight());
        t += keyboard.getTopImageHeight();
        kbBgImage_middle = new BView(kbBgImage, 0, t, keyboard.getImageWidth(), keyboard
                .getMiddleImageHeight());
        t += keyboard.getMiddleImageHeight();
        kbBgImage_bottom = new BView(kbBgImage, 0, t, keyboard.getImageWidth(), keyboard
                .getBottomImageHeight());

        if (showTips) {
            int tipsPadH = keyboard.getTipsPadH();
            int tipsPadV = keyboard.getTipsPadV();
            tipsView = new BView(this, keyboard.getImageWidth() + tipsPadH, spacing + tipsPadV,
                    keyboard.getTipsImageWidth(), keyboard.getTipsImageHeight());
            tipsView.setResource(keyboard.getTipsImageResource());
        }

        replaceKeyboard(keyboard);

        BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
        int padH = e.getInt(P_PAD_H, 5);
        int padV = e.getInt(P_PAD_V, 10);
        BHighlight h = new BHighlight(e.getName(), null, -padH, -padV);
        h.setAbove(true);
        h.setStretchWidth(focused.getWidth() + 2 * padH);
        getKeyboardHighlights().set(h);

        refreshHighlights(animation);
    }
    
    protected BFocusMgrPlus createFocusManager() {
        return new BFocusMgrPlus();
    }

    @Override
    public void remove(Resource animation) {
        PlusSupport.viewRemoveNotify(this);
        super.remove(animation);
    }
    
    protected void viewRemoveNotify() {
        if (getDisposeResourceOnRemove()) {
            Resource resource = getResource();
            if (resource != null) {
                //remove the resource
                resource.remove();
            }
        }
    }

    public boolean getDisposeResourceOnRemove() {
        return disposeResourceOnRemove;
    }

    public void setDisposeResourceOnRemove(boolean disposeResourceOnRemove) {
        this.disposeResourceOnRemove = disposeResourceOnRemove;
    }
    
    public Rectangle getScreenFocusBounds() {
        BPoint s = toScreen();
        int width;
        if (activeKeyboard != null) {
            width = activeKeyboard.getImageWidth();
        } else {
            width = getWidth();
        }
        return new Rectangle(s.x, s.y, width, getHeight());
    }

    private static int getDefaultWidth(Keyboard keyboard, int dataFieldWidth, boolean showTips) {
        int width = keyboard.getImageWidth();
        if (showTips) {
            width += keyboard.getTipsImageWidth() + keyboard.getTipsPadH();
        }
        return Math.max(dataFieldWidth, width);
    }

    /**
     * Set a new key grid, remove the current views associated with the old grid and replace them
     * with new grid.
     * 
     * This will also attempt to keep the highlight in the same place, it first looks for a key with
     * the same value of the old highlight and failing that tries to find a key in the location.
     * 
     * This is a thread-safe public entry point.
     * 
     * @param keyboard
     *            The grid which will be displayed
     */
    protected void replaceKeyboard(Keyboard keyboard) {
        activeKeyboard = keyboard;

        // get the string of the currently focused item
        // after replacing the grid we will look for this and
        // reset the focus

        Rectangle oldFocusBounds = null;
        if (focused != null) {
            oldFocusBounds = focused.getBounds();
        }
        focused = null;

        setPainting(false);

        // set up views into which we will place our images
        kbBgImage.setSize(keyboard.getImageWidth(), keyboard.getImageHeight());
        int t = 0;
        kbBgImage_top.setBounds(0, t, keyboard.getImageWidth(), keyboard.getTopImageHeight());
        t += keyboard.getTopImageHeight();
        kbBgImage_middle.setBounds(0, t, keyboard.getImageWidth(), keyboard
                .getMiddleImageHeight());
        t += keyboard.getMiddleImageHeight();
        kbBgImage_bottom.setBounds(0, t, keyboard.getImageWidth(), keyboard
                .getBottomImageHeight());

        kbBgImage_top.setResource(keyboard.getTopImageResource());
        kbBgImage_middle.setResource(keyboard.getMiddleImageResource());
        kbBgImage_bottom.setResource(keyboard.getBottomImageResource());

        // remove current grid

        boolean tryAgain = true;
        while (tryAgain) {
            int numFound = 0;
            for (int i = 0; i < kbBgImage.getChildCount(); i++) {
                if (kbBgImage.getChild(i) instanceof CellView) {
                    kbBgImage.getChild(i).setVisible(false);
                    kbBgImage.getChild(i).remove();

                    ++numFound;
                    break;
                }
            }
            if (numFound == 0) {
                tryAgain = false;
            }
        }

        // create new grid

        Cell[][] newgrid = keyboard.getCells();
        // find the longest line
        int longest = 0;
        for (int i = 0; i < newgrid.length; i++) {
            if (newgrid[i].length > longest) {
                longest = newgrid[i].length;
            }
        }
        // make a grid of the views
        gridViews = new CellView[newgrid.length][longest];

        for (int i = 0; i < newgrid.length; i++) {
            int view_x = 10;
            for (int j = 0; j < newgrid[i].length; j++) {
                if (newgrid[i][j] != null && newgrid[i][j].type != Cell.CELL_TYPE_EMPTY) {
                    int w = newgrid[i][j].width;

                    gridViews[i][j] = new CellView(kbBgImage, newgrid[i][j]);
                    view_x += w;
                }
            }
        }

        Point defaultPoint = keyboard.getDefaultCell();
        defaultCell = gridViews[defaultPoint.x][defaultPoint.y];

        //
        // if we didn't set the focus by string, set it by location
        // 
        if (oldFocusBounds != null) {
            int centerX = oldFocusBounds.x + (oldFocusBounds.width / 2);
            int centerY = oldFocusBounds.y + (oldFocusBounds.height / 2);
            for (int i = 0; i < kbBgImage.getChildCount(); i++) {
                if (kbBgImage.getChild(i) instanceof CellView) {
                    if (kbBgImage.getChild(i).getBounds().contains(centerX, centerY)) {
                        focused = (CellView) kbBgImage.getChild(i);
                        break;
                    }
                }
            }

        }

        // if all else fails just set the focus to the default item
        if (focused == null) {
            focusDefaultCell();
        }

        setPainting(true);
        flush();

        refreshHighlights(animation);
    } // replaceKeyboard()

    /**
     * move the hilite focus to the currently focused key
     * 
     * This is a thread-safe public entry point.
     */
    protected void refreshHighlights(String anim) {
        if (anim != null) {
            Resource animation = getResource(anim);
            getKeyboardHighlights().refresh(animation);
            getDataInputHighlights().refresh(animation);
        } else {
            getKeyboardHighlights().refresh();
            getDataInputHighlights().refresh();
        }
    }

    public boolean handleEvent(HmeEvent event) {
        if (event instanceof KeyboardEvent) {
            textField.update(((KeyboardEvent) event).getValue());
        }
        return super.handleEvent(event);
    }

    /**
     * Upon gaining the focus we want to make sure the highlight is visible, then hide it when we
     * lose focus.
     * 
     * <p>
     * This is a thread-safe public entry point.
     * </p>
     */
    public boolean handleFocus(boolean gotFocus, BView gained, BView lost) {
        if (gotFocus && gained == this) {
            getScreen().setFocus(focused);
            highlightVis = true;
            refreshHighlights(animation);
        } else if (gotFocus == false && lost == this) {
            highlightVis = false;
            refreshHighlights(animation);
        } else {
            highlightVis = gained instanceof CellView;
            refreshHighlights(animation);
        }

        return super.handleFocus(gotFocus, gained, lost);
    }

    /**
     * repeat keypresses are treated as regular jey presses
     * 
     * <p>
     * This is a thread-safe public entry point.
     * </p>
     */
    public boolean handleKeyRepeat(int code, long rawcode) {
        // synchronization handled by method we are calling
        return handleKeyPress(code, rawcode);
    }

    /**
     * Use the keys to navigate and also allow the user to press the number geys to enter numbers
     * 
     * <p>
     * This is a thread-safe public entry point.
     * </p>
     */
    public boolean handleKeyPress(int code, long rawcode) {
        switch (code) {
        case KEY_RIGHT:
            BView newFocus = focusMgr.followArrow(focused, KEY_RIGHT);
            if (newFocus instanceof CellView) {
                focused = (CellView) newFocus;
                getScreen().setFocus(focused);
                BHighlight h = getKeyboardHighlights().get(H_KEYBOARD);
                BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
                int padH = e.getInt(P_PAD_H, 10);
                h.setStretchWidth(focused.getWidth() + 2 * padH);
                getBApp().play("updown.snd");
                refreshHighlights(animation);
            } else {
                getParent().postEvent(new BEvent.Action(this, "right"));
            }
            return true;
        case KEY_LEFT:
            newFocus = focusMgr.followArrow(focused, KEY_LEFT);
            if (newFocus instanceof CellView) {
                focused = (CellView) newFocus;
                getScreen().setFocus(focused);
                BHighlight h = getKeyboardHighlights().get(H_KEYBOARD);
                BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
                int padH = e.getInt(P_PAD_H, 10);
                h.setStretchWidth(focused.getWidth() + 2 * padH);

                getBApp().play("updown.snd");

                refreshHighlights(animation);
            } else {
                getParent().postEvent(new BEvent.Action(this, "left"));
            }
            return true;
        case KEY_UP:
            newFocus = focusMgr.followArrow(focused, KEY_UP);
            if (newFocus instanceof CellView) {
                focused = (CellView) newFocus;
                getScreen().setFocus(focused);
                BHighlight h = getKeyboardHighlights().get(H_KEYBOARD);
                BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
                int padH = e.getInt(P_PAD_H, 10);
                h.setStretchWidth(focused.getWidth() + 2 * padH);

                getBApp().play("updown.snd");
                refreshHighlights(animation);
            } else {
                getParent().postEvent(new BEvent.Action(this, "up"));
            }
            return true;
        case KEY_DOWN:
            newFocus = focusMgr.followArrow(focused, KEY_DOWN);
            if (newFocus instanceof CellView) {
                focused = (CellView) newFocus;
                getScreen().setFocus(focused);

                BHighlight h = getKeyboardHighlights().get(H_KEYBOARD);
                BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
                int padH = e.getInt(P_PAD_H, 10);
                h.setStretchWidth(focused.getWidth() + 2 * padH);

                getBApp().play("updown.snd");

                refreshHighlights(animation);
            } else {
                getParent().postEvent(new BEvent.Action(this, "down"));
            }
            return true;
        case KEY_SELECT:

            if (focused != null) {
                handleSelection(focused.getType(), focused.getValue());
            }
            return true;
        case KEY_THUMBSUP:
            String thumbsUpName = activeKeyboard.getThumbsUpKeyboard();
            if (thumbsUpName != null) {
                handleSelection(Cell.CELL_TYPE_KEYBOARD, thumbsUpName);
            }
            return true;
        case KEY_THUMBSDOWN:
            String thumbsDownName = activeKeyboard.getThumbsDownKeyboard();
            if (thumbsDownName != null) {
                handleSelection(Cell.CELL_TYPE_KEYBOARD, thumbsDownName);
            }
            return true;
        case KEY_REVERSE:
            handleSelection(Cell.CELL_TYPE_DEL, null);
            return true;
        case KEY_FORWARD:
            handleSelection(Cell.CELL_TYPE_CHAR, " ");
            return true;
        case KEY_NUM0:
            handleSelection(Cell.CELL_TYPE_CHAR, "0");
            return true;
        case KEY_NUM1:
            handleSelection(Cell.CELL_TYPE_CHAR, "1");
            return true;
        case KEY_NUM2:
            handleSelection(Cell.CELL_TYPE_CHAR, "2");
            return true;
        case KEY_NUM3:
            handleSelection(Cell.CELL_TYPE_CHAR, "3");
            return true;
        case KEY_NUM4:
            handleSelection(Cell.CELL_TYPE_CHAR, "4");
            return true;
        case KEY_NUM5:
            handleSelection(Cell.CELL_TYPE_CHAR, "5");
            return true;
        case KEY_NUM6:
            handleSelection(Cell.CELL_TYPE_CHAR, "6");
            return true;
        case KEY_NUM7:
            handleSelection(Cell.CELL_TYPE_CHAR, "7");
            return true;
        case KEY_NUM8:
            handleSelection(Cell.CELL_TYPE_CHAR, "8");
            return true;
        case KEY_NUM9:
            handleSelection(Cell.CELL_TYPE_CHAR, "9");
            return true;
        case KEY_CLEAR:
            handleSelection(Cell.CELL_TYPE_CLR, null);
            return true;
        }
        return super.handleKeyPress(code, rawcode);
    }

    /**
     * Set a keyboard to a keyboard state
     * 
     * a BKeyboard consists of 4 states:
     * 
     * LOWERCASE; UPPERCASE; SYMBOL; NUMCAPSLOCK;
     * 
     * each state needs a keyboard associated with it
     * 
     * If you are changing a keybord of the current state, the new keyboard will be displayed
     * immediately
     * 
     * This is a thread-safe public entry point.
     * 
     * @param state
     *            The state of the keyboard
     * @param keyboard
     *            a keyboard
     */
    public void linkKeyboardToState(String name, Keyboard keyboard) {

        Keyboard existing = keyboardStateMap.get(name);
        keyboardStateMap.put(name, keyboard);

        // if we're replacing the curerent state then replace the keyboard
        if (existing == keyboard) {
            setKeyboard(name);
        }
    }

    public void setKeyboard(String name) {
        Keyboard keyboard = keyboardStateMap.get(name);
        if (keyboard == null) {
            keyboard = new SkinKeyboard((BSkinPlus) getBApp().getSkin(), name);
            keyboardStateMap.put(name, keyboard);
        }
        if (keyboard != activeKeyboard) {
            replaceKeyboard(keyboard);
        }
    }

    /**
     * Handle an item selected from the keyboard, the character selected is passed in
     * 
     * <p>
     * This is a thread-safe public entry point.
     * </p>
     */
    protected void handleSelection(int type, String value) {
        
        //
        // change keyboard and return
        //

        switch (type) {

        case Cell.CELL_TYPE_KEYBOARD:
            setKeyboard(value);
            getBApp().play("select.snd");
            return;

        case Cell.CELL_TYPE_DEL:
            if (this.value.length() > 0) {
                this.value = this.value.substring(0, this.value.length() - 1);
                notifyListeners();
                getBApp().play("select.snd");
            } else {
                getBApp().play("bonk.snd");
            }
            return;
        case Cell.CELL_TYPE_UNDO:
            setValue(undoValue);
            undoValue = null;
            setUndoState(false);
            getBApp().play("select.snd");
            return;
        case Cell.CELL_TYPE_CLR:
            if (this.value.length() > 0) {
                setUndoState(true);
                undoValue = getValue();
                this.value = "";
                notifyListeners();
                getBApp().play("select.snd");
            } else {
                getBApp().play("bonk.snd");
            }
            return;
        case Cell.CELL_TYPE_CHAR:
            setValue(this.value += value);
            getBApp().play("select.snd");
            break;
        }
    }

    /**
     * Tell all the clients that the text associated with the keyboard has changed
     * 
     * <p>
     * This is a thread-safe public entry point.
     * </p>
     */
    protected void notifyListeners() {
        if (getValue() != null) {
            postEvent(new KeyboardEvent(EVT_KBD_VALUE, this.getID(), getValue()));
        }
    }

    /**
     * Get the value of the widget, a string representing what has been entered
     * 
     * This is a thread-safe public entry point.
     * 
     * @return the string that has been typed in
     */
    public String getValue() {
        // getter methods generally don't need synchronization
        return value;
    }

    /**
     * Set the value of the widget, all clients will be notified imediately of the change
     * 
     * @param newWord
     *            New valuee of the widget
     */
    public void setValue(String newWord) {
        if (newWord == null) {
            newWord = "";
        }
        if (undoValue != null) {
            setUndoState(false);
        }
        this.value = newWord;
        notifyListeners();
    }

    /**
     * 
     * This is a thread-safe public entry point.
     * 
     * @param undo
     */
    protected void setUndoState(boolean undo) {
        Keyboard kb = activeKeyboard;
        if (undo == true) {
            Resource newImage = kb.getTopUndoImageResource();
            if (newImage == null) {
                return;

            }
            // scan first occurrence for CLR, replace with UNDO
            CellView[][] cells = this.gridViews;
            for (int i = 0; i < cells.length; i++) {
                CellView[] row = cells[i];
                for (int j = 0; j < row.length; j++) {
                    if (row[j] != null && row[j].getType() == Cell.CELL_TYPE_CLR) {
                        kbBgImage_top.setResource(newImage);
                        row[j].setType(Cell.CELL_TYPE_UNDO);
                        break;
                    }
                }
            }
        } else {
            Resource newImage = kb.getTopImageResource();
            // scan first occurrence of an overridden type and reset
            CellView[][] cells = this.gridViews;
            for (int i = 0; i < cells.length; i++) {
                CellView[] row = cells[i];
                for (int j = 0; j < row.length; j++) {
                    if (row[j] != null && row[j].isTypeOverridden()) {
                        kbBgImage_top.setResource(newImage);
                        row[j].resetType();
                        break;
                    }
                }
            }
        }
    }

    /**
     * set focus on the default cell
     */
    public void focusDefaultCell() {
        if (defaultCell == null) {
            // this should never happen;
            return;
        }

        focused = defaultCell;
        getScreen().setFocus(focused);
        BHighlights keyboardHighlights = getKeyboardHighlights();
        if (keyboardHighlights != null) {
            BHighlight h = keyboardHighlights.get(H_KEYBOARD);
            if (h != null) {
                BSkin.Element e = getBApp().getSkin().get(H_KEYBOARD);
                int padH = e.getInt(P_PAD_H, 10);
                h.setStretchWidth(focused.getWidth() + 2 * padH);
            }
        }
        refreshHighlights(animation);
    }

    public Keyboard getActiveKeyboard() {
        return activeKeyboard;
    }

    public BHighlightsPlus getKeyboardHighlights() {
        if (keyboardHighlights == null) {
            keyboardHighlights = createKeyboardHighlights();
        }
        return keyboardHighlights;
    }

    public void setKeyboardHighlights(BHighlightsPlus keyboardHighlights) {
        this.keyboardHighlights = keyboardHighlights;
        refreshHighlights(animation);
    }

    protected BHighlightsPlus createKeyboardHighlights() {
        return new BHighlightsPlus(new HighlightsLayout());
    }

    public BHighlightsPlus getDataInputHighlights() {
        if (dataInputHighlights == null) {
            dataInputHighlights = createDataInputHighlights();
        }
        return dataInputHighlights;
    }

    public void setDataInputHighlights(BHighlightsPlus dataInputHighlights) {
        this.dataInputHighlights = dataInputHighlights;
        refreshHighlights(animation);
    }

    protected BHighlightsPlus createDataInputHighlights() {
        return new BHighlightsPlus(new InputLayout());
    }

    public class InputLayout implements IHighlightsLayout {

        public BScreen getScreen() {
            return BKeyboardPlus.this.getScreen();
        }

        public BRect getHighlightBounds() {
            Rectangle bounds = dataField.getBounds();
            BRect rect = new BRect(bounds.x, bounds.y, bounds.width, bounds.height);
            return toScreenBounds(rect);
        }

        public boolean getHighlightIsVisible(int visible) {
            return dataField.getHighlightIsVisible(visible);
        }
    }

    public class HighlightsLayout implements IHighlightsLayout {

        public BScreen getScreen() {
            return BKeyboardPlus.this.getScreen();
        }

        public BRect getHighlightBounds() {
            // synchronization handled by method we are calling
            return getFocusBounds();
        }

        /**
         * <p>
         * This is a thread-safe public entry point.
         * </p>
         */
        public BRect getFocusBounds() {

            if (focused != null) {
                BRect rect = focused.getHighlightBounds();
                return rect;
            }

            return toScreenBounds(new BRect(0, 0, getWidth(),
                    activeKeyboard.getCells()[0][0].height));
        }

        public boolean getHighlightIsVisible(int visible) {
            return highlightVis;
        }
    }

    public static class DisplayText extends BTextPlus<String> {

        protected HmeEvent.FontInfo fi;

        protected int curAlignment = RSRC_HALIGN_LEFT;

        /**
         * Constructor
         * 
         * <p>
         * This is a thread-safe public entry point.
         * </p>
         */
        public DisplayText(BView parent, int x, int y, int width, int height, Resource font,
                Resource color) {
            super(parent, x, y, width, height);
            setFont(font);
            setColor(color);
            update("");

            font.addHandler(this);
        }

        /*
         * <p>This is a thread-safe public entry point.</p>
         * 
         * @see com.tivo.hme.bananas.IKeyboardClient#update(java.lang.String)
         */
        public void update(String word) {
            setPainting(false);
            String newWord = word + "_";
            if (fi != null) {
                int i = fi.measureTextWidth(newWord);
                if (i > this.getWidth()) {
                    if (curAlignment != RSRC_HALIGN_RIGHT) {
                        this.setFlags(RSRC_HALIGN_RIGHT);
                        curAlignment = RSRC_HALIGN_RIGHT;
                    }
                } else {
                    if (curAlignment != RSRC_HALIGN_LEFT) {
                        this.setFlags(RSRC_HALIGN_LEFT);
                        curAlignment = RSRC_HALIGN_LEFT;
                    }
                }
                setValue(newWord);
            } else {
                setValue(newWord);
            }
            setPainting(true);
        } // update()

        /**
         * <p>
         * This is a thread-safe public entry point.
         * </p>
         */
        public boolean handleEvent(HmeEvent event) {
            switch (event.getOpCode()) {
            case EVT_FONT_INFO:
                fi = (HmeEvent.FontInfo) event;
                return true;
            }

            return false;
        } // handleEvent()
    }

    public static class CellView extends BView {
        protected Cell cell;

        private int type;

        /**
         * Constructor
         */
        public CellView(BView parent, Cell cell) {
            super(parent, cell.x, cell.y, cell.width, cell.height);
            this.cell = cell;
            this.type = cell.type;
            setFocusable(true);
        }

        public boolean isTypeOverridden() {
            return type != cell.type;
        }

        public void resetType() {
            setType(cell.type);
        }

        public int getType() {
            return type;
        }

        public void setType(int type) {
            this.type = type;
        }

        /**
         * Never returns null.
         */
        public String getValue() {
            switch (type) {
            case Cell.CELL_TYPE_KEYBOARD:
                return cell.value;
            case Cell.CELL_TYPE_CLR:
                return "CLR";
            case Cell.CELL_TYPE_EMPTY:
                return "EMPTY";
            case Cell.CELL_TYPE_UNDO:
                return "UNDO";
            case Cell.CELL_TYPE_CHAR:
                if (cell.value == null) {
                    return "";
                }
                return cell.value;
            }
            return "";
        }

        public String toString() {
            return "" + cell.value;
        }
    }

    public static class Cell {
        public static final int CELL_TYPE_DEL = 1;

        public static final int CELL_TYPE_CLR = 2;

        public static final int CELL_TYPE_KEYBOARD = 3;

        public static final int CELL_TYPE_UNDO = 4;

        public static final int CELL_TYPE_CHAR = 7;

        public static final int CELL_TYPE_EMPTY = 8;

        int type = CELL_TYPE_EMPTY;

        String value = null;

        int x;

        int y;

        int width;

        int height;

        public Cell(int type, String value, int x, int y, int width, int height) {
            this.type = type;
            this.value = value;
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
        }
    }

    public static class KeyboardEvent extends HmeEvent {
        private String value;

        /**
         * This constructor does not access class-static variables, so it does not need to be
         * synchronized.
         * 
         * @param opcode
         * @param id
         */
        public KeyboardEvent(int opcode, int id, String value) {
            super(opcode, id);
            this.value = value;
        }

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }
    }

    public static interface Keyboard {
        public String getName();

        public Point getDefaultCell();

        public int getImageHeight();

        public int getImageWidth();

        public Cell[][] getCells();

        public Resource getBottomImageResource();

        public int getBottomImageHeight();

        public Resource getMiddleImageResource();

        public int getMiddleImageHeight();

        public Resource getTopImageResource();

        public int getTopImageHeight();

        public Resource getTopUndoImageResource();

        public Resource getDataInputImageResource();

        public int getDataInputImageHeight();

        public int getDataInputImageWidth();

        public int getDataInputCap();

        public int getDataInputPadH();

        public int getDataInputPadV();

        public Resource getTipsImageResource();

        public int getTipsImageWidth();

        public int getTipsImageHeight();

        public int getTipsPadH();

        public int getTipsPadV();

        public Resource getFontResource();

        public Resource getColorResource();

        public String getThumbsUpKeyboard();

        public String getThumbsDownKeyboard();
    }

    public static class SkinKeyboard implements Keyboard {
        protected BSkinPlus skin;

        protected String name;

        protected String thumbsUpName;

        protected String thumbsDownName;

        public SkinKeyboard(BSkinPlus skin, String name) {
            this.skin = skin;
            this.name = name;
            int index = name.lastIndexOf('-');
            if (index != -1) {
                String prefix = name.substring(index + 1);
                thumbsDownName = prefix + LOWERCASE;
                thumbsUpName = prefix + UPPERCASE;
            } else {
                thumbsDownName = LOWERCASE;
                thumbsUpName = UPPERCASE;
            }

            //allow skin to override key mappings
            Element e = skin.get(name + H_KEYBOARD_TOP_IMAGE_SUFFIX);
            if (e.containsKey(P_KEYBOARD_THUMBS_UP)) {
                thumbsUpName = e.get(P_KEYBOARD_THUMBS_UP);
            }
            if (e.containsKey(P_KEYBOARD_THUMBS_DOWN)) {
                thumbsDownName = e.get(P_KEYBOARD_THUMBS_DOWN);
            }
        }

        public String getThumbsUpKeyboard() {
            return thumbsUpName;
        }

        public String getThumbsDownKeyboard() {
            return thumbsDownName;
        }

        public String getName() {
            return name;
        }

        /**
         * Get a cell that is considered the default so that upon switching to a new layout, the
         * keyboard knows where to place the highlight
         * 
         * @return The cell that should be focused initially or when the keyboard is cleared
         */
        public Point getDefaultCell() {
            return new Point(0, 0);
        }

        public int getImageHeight() {
            return getTopImageHeight() + getMiddleImageHeight() + getBottomImageHeight()
                    + getDataInputImageHeight() + getDataInputPadV();
        }

        public int getImageWidth() {
            Element e = skin.get(name + H_KEYBOARD_TOP_IMAGE_SUFFIX);
            if (e != null) {
                return e.getWidth();
            } else {
                return 0;
            }
        }

        public Cell[][] getCells() {
            Element e = skin.get(name + H_KEYBOARD_TOP_IMAGE_SUFFIX);
            if (e != null) {
                return (Cell[][]) e.getObject("cells");
            } else {
                return null;
            }
        }

        public Resource getBottomImageResource() {
            Element e = skin.get(name + H_KEYBOARD_BOTTOM_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public int getBottomImageHeight() {
            Element e = skin.get(name + H_KEYBOARD_BOTTOM_IMAGE_SUFFIX);
            if (e != null) {
                return e.getHeight();
            } else {
                return 0;
            }
        }

        public Resource getMiddleImageResource() {
            Element e = skin.get(name + H_KEYBOARD_MIDDLE_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public int getMiddleImageHeight() {
            Element e = skin.get(name + H_KEYBOARD_MIDDLE_IMAGE_SUFFIX);
            if (e != null) {
                return e.getHeight();
            } else {
                return 0;
            }
        }

        public Resource getTopImageResource() {
            Element e = skin.get(name + H_KEYBOARD_TOP_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public int getTopImageHeight() {
            Element e = skin.get(name + H_KEYBOARD_TOP_IMAGE_SUFFIX);
            if (e != null) {
                return e.getHeight();
            } else {
                return 0;
            }
        }

        public Resource getTopUndoImageResource() {
            Element e = skin.get(name + H_KEYBOARD_TOP_UNDO_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public Resource getDataInputImageResource() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public int getDataInputImageWidth() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getWidth();
            } else {
                return 0;
            }
        }

        public int getDataInputImageHeight() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getHeight();
            } else {
                return 0;
            }
        }

        public int getDataInputPadH() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getInt(P_PAD_H);
            } else {
                return 0;
            }
        }

        public int getDataInputCap() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getInt(P_CAP);
            } else {
                return 0;
            }
        }

        public int getDataInputPadV() {
            Element e = skin.get(name + H_KEYBOARD_DATA_INPUT_IMAGE_SUFFIX);
            if (e != null) {
                return e.getInt(P_PAD_V);
            } else {
                return 0;
            }
        }

        public Resource getFontResource() {
            Element e = skin.get(name + H_KEYBOARD_FONT_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public Resource getColorResource() {
            Element e = skin.get(name + H_KEYBOARD_COLOR_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public Resource getTipsImageResource() {
            Element e = skin.get(name + H_KEYBOARD_TIPS_IMAGE_SUFFIX);
            if (e != null) {
                return e.getResource();
            } else {
                return null;
            }
        }

        public int getTipsImageWidth() {
            Element e = skin.get(name + H_KEYBOARD_TIPS_IMAGE_SUFFIX);
            if (e != null) {
                return e.getWidth();
            } else {
                return 0;
            }
        }

        public int getTipsImageHeight() {
            Element e = skin.get(name + H_KEYBOARD_TIPS_IMAGE_SUFFIX);
            if (e != null) {
                return e.getHeight();
            } else {
                return 0;
            }
        }

        public int getTipsPadH() {
            Element e = skin.get(name + H_KEYBOARD_TIPS_IMAGE_SUFFIX);
            if (e != null) {
                return e.getInt(P_PAD_H, 0);
            } else {
                return 0;
            }
        }

        public int getTipsPadV() {
            Element e = skin.get(name + H_KEYBOARD_TIPS_IMAGE_SUFFIX);
            if (e != null) {
                return e.getInt(P_PAD_V, 0);
            } else {
                return 0;
            }
        }
    }

}
